5.3.3 结构类型
结构类型可以用来描述一组数据值,这组值的本质即可以是原始的,也可以是非原始的。如果决定在某些东西需要删除或者添加某个结构类型的值时该结构类型的值不应该被更改,那么需要遵守之前提到的内置类型和引用类型的规范。让我们从标准库里的一个原始本质的类型的结构实现开始,如代码清单5-28所示。
代码清单5-28 golang.org/src/time/time.go:第39行到第55行
39 type Time struct {
40 // sec给出自公元1年1月1日00:00:00
41 // 开始的秒数
42 sec int64
43
44 // nsec指定了一秒内的纳秒偏移,
45 // 这个值是非零值,
46 // 必须在[0, 999999999]范围内
47 nsec int32
48
49 // loc指定了一个Location,
50 // 用于决定该时间对应的当地的分、小时、
51 // 天和年的值
52 // 只有Time的零值,其loc的值是nil
53 // 这种情况下,认为处于UTC时区
54 loc *Location
55 }
代码清单5-28中的 Time
结构选自 time
包。当思考时间的值时,你应该意识到给定的一个时间点的时间是不能修改的。所以标准库里也是这样实现 Time
类型的。让我们看一下 Now
函数是如何创建 Time
类型的值的,如代码清单5-29所示。
代码清单5-29 golang.org/src/time/time.go:第781行到第784行
781 func Now() Time {
782 sec, nsec := now()
783 return Time{sec + unixToInternal, nsec, Local}
784 }
代码清单5-29中的代码展示了 Now
函数的实现。这个函数创建了一个 Time
类型的值,并给调用者返回了 Time
值的副本。这个函数没有使用指针来共享 Time
值。之后,让我们来看一个 Time
类型的方法,如代码清单5-30所示。
代码清单5-30 golang.org/src/time/time.go:第610行到第622行
610 func (t Time) Add(d Duration) Time {
611 t.sec += int64(d / 1e9)
612 nsec := int32(t.nsec) + int32(d%1e9)
613 if nsec >= 1e9 {
614 t.sec++
615 nsec -= 1e9
616 } else if nsec < 0 {
617 t.sec--
618 nsec += 1e9
619 }
620 t.nsec = nsec
621 return t
622 }
代码清单5-30中的 Add
方法是展示标准库如何将 Time
类型作为本质是原始的类型的绝佳例子。这个方法使用值接收者,并返回了一个新的 Time
值。该方法操作的是调用者传入的 Time
值的副本,并且给调用者返回了一个方法内的 Time
值的副本。至于是使用返回的值替换原来的 Time
值,还是创建一个新的 Time
变量来保存结果,是由调用者决定的事情。
大多数情况下,结构类型的本质并不是原始的,而是非原始的。这种情况下,对这个类型的值做增加或者删除的操作应该更改值本身。当需要修改值本身时,在程序中其他地方,需要使用指针来共享这个值。让我们看一个由标准库中实现的具有非原始本质的结构类型的例子,如代码清单5-31所示。
代码清单5-31 golang.org/src/os/file_unix.go:第15行到第29行
15 // File表示一个打开的文件描述符
16 type File struct {
17 *file
18 }
19
20 // file是*File的实际表示
21 // 额外的一层结构保证没有哪个os的客户端
22 // 能够覆盖这些数据。如果覆盖这些数据,
23 // 可能在变量终结时关闭错误的文件描述符
24 type file struct {
25 fd int
26 name string
27 dirinfo *dirInfo // 除了目录结构,此字段为nil
28 nepipe int32 // Write操作时遇到连续EPIPE的次数
29 }
可以在代码清单5-31里看到标准库中声明的 File
类型。这个类型的本质是非原始的。这个类型的值实际上不能安全复制。对内部未公开的类型的注释,解释了不安全的原因。因为没有方法阻止程序员进行复制,所以 File
类型的实现使用了一个嵌入的指针,指向一个未公开的类型。本章后面会继续探讨内嵌类型。正是这层额外的内嵌类型阻止了复制。不是所有的结构类型都需要或者应该实现类似的额外保护。程序员需要能识别出每个类型的本质,并使用这个本质来决定如何组织类型。
让我们看一下 Open
函数的实现,如代码清单5-32所示。
代码清单5-32 golang.org/src/os/file.go:第238行到第240行
238 func Open(name string) (file *File, err error) {
239 return OpenFile(name, O_RDONLY, 0)
240 }
代码清单5-32展示了 Open
函数的实现,调用者得到的是一个指向 File
类型值的指针。 Open
创建了 File
类型的值,并返回指向这个值的指针。如果一个创建用的工厂函数返回了一个指针,就表示这个被返回的值的本质是非原始的。
即便函数或者方法没有直接改变非原始的值的状态,依旧应该使用共享的方式传递,如代码清单5-33所示。
代码清单5-33 golang.org/src/os/file.go:第224行到第232行
224 func (f *File) Chdir() error {
225 if f == nil {
226 return ErrInvalid
227 }
228 if e := syscall.Fchdir(f.fd); e != nil {
229 return &PathError{"chdir", f.name, e}
230 }
231 return nil
232 }
代码清单5-33中的 Chdir
方法展示了,即使没有修改接收者的值,依然是用指针接收者来声明的。因为 File
类型的值具备非原始的本质,所以总是应该被共享,而不是被复制。
是使用值接收者还是指针接收者,不应该由该方法是否修改了接收到的值来决定。这个决策应该基于该类型的本质。这条规则的一个例外是,需要让类型值符合某个接口的时候,即便类型的本质是非原始本质的,也可以选择使用值接收者声明方法。这样做完全符合接口值调用方法的机制。5.4节会讲解什么是接口值,以及使用接口值调用方法的机制。